<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>流水线构建</title>
<script src="https://cdn.tailwindcss.com/3.4.16"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/remixicon/4.6.0/remixicon.min.css" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#6B7280'
},
borderRadius: {
'none': '0px',
'sm': '4px',
DEFAULT: '8px',
'md': '12px',
'lg': '16px',
'xl': '20px',
'2xl': '24px',
'3xl': '32px',
'full': '9999px',
'button': '8px'
}
}
}
}
</script>
<style>
:where([class^="ri-"])::before { content: "\f3c2"; }
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
.custom-scrollbar::-webkit-scrollbar-thumb { background: #E5E7EB; border-radius: 4px; }
.canvas-grid {
background-image: radial-gradient(circle, #e5e7eb 1px, transparent 1px);
background-size: 20px 20px;
}
.model-node {
transition: all 0.2s ease;
cursor: move;
}
.model-node:hover {
transform: scale(1.02);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.connection-line {
stroke: #3B82F6;
stroke-width: 2;
fill: none;
marker-end: url(#arrowhead);
}
.dragging {
opacity: 0.8;
transform: scale(0.95);
}
.drop-zone {
border: 2px dashed #3B82F6;
background: rgba(59, 130, 246, 0.05);
}
</style>
</head>
<body class="bg-gray-50">
<div class="flex h-screen">
<aside class="w-80 bg-white border-r border-gray-200 flex flex-col">
<div class="p-4 border-b border-gray-200">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-medium">模型组件库</h2>
<a href="https://readdy.ai/home/42b69b4b-e314-4f8d-9e2f-98a4952face1/83af26fe-6a2c-4b4d-9dc6-9dd0ca282c18" data-readdy="true" class="text-gray-500 hover:text-gray-700">
<div class="w-8 h-8 flex items-center justify-center">
<i class="ri-arrow-left-line"></i>
</div>
</a>
</div>
<div class="relative">
<input type="text" id="modelSearch" placeholder="搜索模型..." class="w-full pl-9 pr-4 py-2 text-sm bg-gray-50 rounded-button focus:outline-none focus:ring-2 focus:ring-primary">
<div class="absolute left-3 top-2.5 w-4 h-4 flex items-center justify-center text-gray-400">
<i class="ri-search-line"></i>
</div>
</div>
</div>
<nav class="flex-1 overflow-y-auto custom-scrollbar">
<div class="p-2">
<div class="space-y-2">
<div class="model-category">
<div class="flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 bg-gray-50 rounded-button cursor-pointer" onclick="toggleCategory('image')">
<div class="flex items-center">
<div class="w-4 h-4 flex items-center justify-center mr-2">
<i class="ri-image-line"></i>
</div>
<span>图像处理模型</span>
</div>
<i class="ri-arrow-down-s-line transform transition-transform" id="image-arrow"></i>
</div>
<div class="category-content mt-2 space-y-2" id="image-models">
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="image" data-model-name="ResNet-50">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-blue-100 flex items-center justify-center mr-3">
<i class="ri-camera-line text-blue-600"></i>
</div>
<div>
<div class="text-sm font-medium">ResNet-50</div>
<div class="text-xs text-gray-500">图像分类</div>
</div>
</div>
<div class="text-xs text-gray-600">深度残差网络，用于图像分类和特征提取</div>
</div>
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="image" data-model-name="YOLO v8">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-green-100 flex items-center justify-center mr-3">
<i class="ri-focus-3-line text-green-600"></i>
</div>
<div>
<div class="text-sm font-medium">YOLO v8</div>
<div class="text-xs text-gray-500">目标检测</div>
</div>
</div>
<div class="text-xs text-gray-600">实时目标检测与定位</div>
</div>
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="image" data-model-name="Stable Diffusion">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-purple-100 flex items-center justify-center mr-3">
<i class="ri-palette-line text-purple-600"></i>
</div>
<div>
<div class="text-sm font-medium">Stable Diffusion</div>
<div class="text-xs text-gray-500">图像生成</div>
</div>
</div>
<div class="text-xs text-gray-600">基于文本描述生成高质量图像</div>
</div>
</div>
</div>
<div class="model-category">
<div class="flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 bg-gray-50 rounded-button cursor-pointer" onclick="toggleCategory('nlp')">
<div class="flex items-center">
<div class="w-4 h-4 flex items-center justify-center mr-2">
<i class="ri-chat-3-line"></i>
</div>
<span>NLP 模型</span>
</div>
<i class="ri-arrow-down-s-line transform transition-transform" id="nlp-arrow"></i>
</div>
<div class="category-content mt-2 space-y-2 hidden" id="nlp-models">
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="nlp" data-model-name="BERT">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-indigo-100 flex items-center justify-center mr-3">
<i class="ri-file-text-line text-indigo-600"></i>
</div>
<div>
<div class="text-sm font-medium">BERT</div>
<div class="text-xs text-gray-500">文本理解</div>
</div>
</div>
<div class="text-xs text-gray-600">双向编码器表示，用于文本分类和理解</div>
</div>
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="nlp" data-model-name="GPT-4">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-emerald-100 flex items-center justify-center mr-3">
<i class="ri-quill-pen-line text-emerald-600"></i>
</div>
<div>
<div class="text-sm font-medium">GPT-4</div>
<div class="text-xs text-gray-500">文本生成</div>
</div>
</div>
<div class="text-xs text-gray-600">大型语言模型，支持对话和文本生成</div>
</div>
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="nlp" data-model-name="T5">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-orange-100 flex items-center justify-center mr-3">
<i class="ri-translate-2 text-orange-600"></i>
</div>
<div>
<div class="text-sm font-medium">T5</div>
<div class="text-xs text-gray-500">文本转换</div>
</div>
</div>
<div class="text-xs text-gray-600">文本到文本转换，支持翻译、摘要等任务</div>
</div>
</div>
</div>
<div class="model-category">
<div class="flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 bg-gray-50 rounded-button cursor-pointer" onclick="toggleCategory('audio')">
<div class="flex items-center">
<div class="w-4 h-4 flex items-center justify-center mr-2">
<i class="ri-volume-up-line"></i>
</div>
<span>语音处理模型</span>
</div>
<i class="ri-arrow-down-s-line transform transition-transform" id="audio-arrow"></i>
</div>
<div class="category-content mt-2 space-y-2 hidden" id="audio-models">
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="audio" data-model-name="Whisper">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-cyan-100 flex items-center justify-center mr-3">
<i class="ri-mic-line text-cyan-600"></i>
</div>
<div>
<div class="text-sm font-medium">Whisper</div>
<div class="text-xs text-gray-500">语音识别</div>
</div>
</div>
<div class="text-xs text-gray-600">多语言语音转文本模型</div>
</div>
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="audio" data-model-name="Tacotron 2">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-pink-100 flex items-center justify-center mr-3">
<i class="ri-speaker-line text-pink-600"></i>
</div>
<div>
<div class="text-sm font-medium">Tacotron 2</div>
<div class="text-xs text-gray-500">语音合成</div>
</div>
</div>
<div class="text-xs text-gray-600">高质量文本到语音转换</div>
</div>
</div>
</div>
<div class="model-category">
<div class="flex items-center justify-between px-3 py-2 text-sm font-medium text-gray-700 bg-gray-50 rounded-button cursor-pointer" onclick="toggleCategory('general')">
<div class="flex items-center">
<div class="w-4 h-4 flex items-center justify-center mr-2">
<i class="ri-cpu-line"></i>
</div>
<span>通用 AI 模型</span>
</div>
<i class="ri-arrow-down-s-line transform transition-transform" id="general-arrow"></i>
</div>
<div class="category-content mt-2 space-y-2 hidden" id="general-models">
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="general" data-model-name="CLIP">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-violet-100 flex items-center justify-center mr-3">
<i class="ri-links-line text-violet-600"></i>
</div>
<div>
<div class="text-sm font-medium">CLIP</div>
<div class="text-xs text-gray-500">多模态</div>
</div>
</div>
<div class="text-xs text-gray-600">图像和文本的联合理解模型</div>
</div>
<div class="model-item bg-white border border-gray-200 rounded-lg p-3 cursor-move hover:border-primary transition-colors" draggable="true" data-model-type="general" data-model-name="Decision Tree">
<div class="flex items-center mb-2">
<div class="w-8 h-8 rounded bg-teal-100 flex items-center justify-center mr-3">
<i class="ri-node-tree text-teal-600"></i>
</div>
<div>
<div class="text-sm font-medium">Decision Tree</div>
<div class="text-xs text-gray-500">决策分析</div>
</div>
</div>
<div class="text-xs text-gray-600">基于树形结构的决策分析模型</div>
</div>
</div>
</div>
</div>
</div>
</nav>
</aside>
<main class="flex-1 flex flex-col overflow-hidden">
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
<div class="flex items-center">
<a href="https://readdy.ai/home/42b69b4b-e314-4f8d-9e2f-98a4952face1/83af26fe-6a2c-4b4d-9dc6-9dd0ca282c18" data-readdy="true" class="mr-4 text-gray-500 hover:text-gray-700">
<div class="w-8 h-8 flex items-center justify-center">
<i class="ri-arrow-left-line"></i>
</div>
</a>
<h1 class="text-lg font-medium">流水线构建</h1>
</div>
<div class="flex items-center space-x-3">
<button id="testPipeline" class="flex items-center px-4 py-2 bg-green-600 text-white rounded-button hover:bg-green-700 transition-colors whitespace-nowrap !rounded-button">
<div class="w-4 h-4 flex items-center justify-center mr-1">
<i class="ri-play-line"></i>
</div>
测试运行
</button>
<button id="savePipeline" class="flex items-center px-4 py-2 bg-primary text-white rounded-button hover:bg-blue-700 transition-colors whitespace-nowrap !rounded-button">
<div class="w-4 h-4 flex items-center justify-center mr-1">
<i class="ri-save-line"></i>
</div>
保存
</button>
<button id="deployPipeline" class="flex items-center px-4 py-2 bg-purple-600 text-white rounded-button hover:bg-purple-700 transition-colors whitespace-nowrap !rounded-button">
<div class="w-4 h-4 flex items-center justify-center mr-1">
<i class="ri-rocket-line"></i>
</div>
部署
</button>
<button id="clearCanvas" class="flex items-center px-4 py-2 bg-gray-600 text-white rounded-button hover:bg-gray-700 transition-colors whitespace-nowrap !rounded-button">
<div class="w-4 h-4 flex items-center justify-center mr-1">
<i class="ri-delete-bin-line"></i>
</div>
清空
</button>
</div>
</header>
<div class="flex-1 flex">
<div class="flex-1 flex flex-col">
<div class="flex-1 relative bg-white canvas-grid overflow-hidden" id="pipelineCanvas">
<svg class="absolute inset-0 w-full h-full pointer-events-none" id="connectionSvg">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#3B82F6" />
</marker>
</defs>
</svg>
<div class="absolute inset-0 p-6" id="canvasContainer">
<div class="text-center text-gray-400 mt-20">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center">
<i class="ri-drag-drop-line text-2xl"></i>
</div>
<p class="text-lg">拖拽模型组件到此处开始构建流水线</p>
<p class="text-sm mt-2">从左侧选择合适的模型组件，拖拽到画布中进行组合</p>
</div>
</div>
</div>
<div class="h-80 bg-gray-50 border-t border-gray-200 p-4">
<div class="h-full bg-white rounded-lg border border-gray-200 p-4" id="configPanel">
<div class="text-center text-gray-400 h-full flex flex-col items-center justify-center">
<div class="w-12 h-12 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
<i class="ri-settings-3-line text-xl"></i>
</div>
<p class="text-sm">选择模型节点查看和配置参数</p>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script id="dragDropScript">
document.addEventListener('DOMContentLoaded', function() {
let draggedElement = null;
let selectedNode = null;
let nodeCounter = 0;
let connections = [];
let isConnecting = false;
let connectionStart = null;
const canvas = document.getElementById('canvasContainer');
const svg = document.getElementById('connectionSvg');
const configPanel = document.getElementById('configPanel');
document.querySelectorAll('.model-item').forEach(item => {
item.addEventListener('dragstart', function(e) {
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'copy';
});
item.addEventListener('dragend', function() {
this.classList.remove('dragging');
draggedElement = null;
});
});
canvas.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
canvas.addEventListener('drop', function(e) {
e.preventDefault();
if (draggedElement) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left - 75;
const y = e.clientY - rect.top - 40;
createModelNode(draggedElement, x, y);
}
});
function createModelNode(modelElement, x, y) {
nodeCounter++;
const modelName = modelElement.dataset.modelName;
const modelType = modelElement.dataset.modelType;
const iconElement = modelElement.querySelector('i');
const iconClass = iconElement.className;
const colorClass = modelElement.querySelector('.w-8').className;
const node = document.createElement('div');
node.className = 'model-node absolute bg-white border-2 border-gray-200 rounded-lg p-4 w-36 shadow-lg cursor-pointer';
node.style.left = x + 'px';
node.style.top = y + 'px';
node.dataset.nodeId = 'node_' + nodeCounter;
node.dataset.modelType = modelType;
node.innerHTML = `
<div class="flex flex-col items-center text-center">
<div class="${colorClass} mb-2">
<i class="${iconClass}"></i>
</div>
<div class="text-sm font-medium mb-1">${modelName}</div>
<div class="text-xs text-gray-500">${modelType}</div>
</div>
<div class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center cursor-pointer opacity-0 hover:opacity-100 transition-opacity" onclick="removeNode(this)">
<i class="ri-close-line text-white text-xs"></i>
</div>
<div class="absolute top-1/2 -left-2 w-4 h-4 bg-blue-500 rounded-full cursor-pointer connection-point input-point" data-type="input"></div>
<div class="absolute top-1/2 -right-2 w-4 h-4 bg-blue-500 rounded-full cursor-pointer connection-point output-point" data-type="output"></div>
`;
canvas.appendChild(node);
makeDraggable(node);
makeConnectable(node);
node.addEventListener('click', function(e) {
e.stopPropagation();
selectNode(this);
});
if (canvas.querySelector('.text-center.text-gray-400')) {
canvas.querySelector('.text-center.text-gray-400').remove();
}
}
function makeDraggable(element) {
let isDragging = false;
let startX, startY, initialX, initialY;
element.addEventListener('mousedown', function(e) {
if (e.target.classList.contains('connection-point')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
initialX = parseInt(element.style.left);
initialY = parseInt(element.style.top);
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);
e.preventDefault();
});
function drag(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
element.style.left = (initialX + dx) + 'px';
element.style.top = (initialY + dy) + 'px';
updateConnections();
}
function stopDrag() {
isDragging = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
}
}
function makeConnectable(node) {
const connectionPoints = node.querySelectorAll('.connection-point');
connectionPoints.forEach(point => {
point.addEventListener('mousedown', function(e) {
e.stopPropagation();
if (isConnecting) {
if (connectionStart && connectionStart !== point) {
createConnection(connectionStart, point);
isConnecting = false;
connectionStart = null;
}
} else {
isConnecting = true;
connectionStart = point;
}
});
});
}
function createConnection(startPoint, endPoint) {
const startNode = startPoint.closest('.model-node');
const endNode = endPoint.closest('.model-node');
if (startNode === endNode) return;
if (startPoint.dataset.type === endPoint.dataset.type) return;
const connection = {
id: 'conn_' + Date.now(),
start: startPoint,
end: endPoint,
startNode: startNode,
endNode: endNode
};
connections.push(connection);
updateConnections();
}
function updateConnections() {
svg.innerHTML = svg.querySelector('defs').outerHTML;
connections.forEach(conn => {
const startRect = conn.start.getBoundingClientRect();
const endRect = conn.end.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
const startX = startRect.left - canvasRect.left + startRect.width / 2;
const startY = startRect.top - canvasRect.top + startRect.height / 2;
const endX = endRect.left - canvasRect.left + endRect.width / 2;
const endY = endRect.top - canvasRect.top + endRect.height / 2;
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const midX = (startX + endX) / 2;
const d = `M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`;
path.setAttribute('d', d);
path.setAttribute('class', 'connection-line');
path.setAttribute('data-connection-id', conn.id);
path.style.cursor = 'pointer';
path.addEventListener('click', function() {
removeConnection(conn.id);
});
svg.appendChild(path);
});
}
function removeConnection(connectionId) {
connections = connections.filter(conn => conn.id !== connectionId);
updateConnections();
}
function selectNode(node) {
document.querySelectorAll('.model-node').forEach(n => {
n.classList.remove('ring-2', 'ring-primary');
});
node.classList.add('ring-2', 'ring-primary');
selectedNode = node;
showNodeConfig(node);
}
function showNodeConfig(node) {
const modelName = node.querySelector('.text-sm.font-medium').textContent;
const modelType = node.dataset.modelType;
configPanel.innerHTML = `
<div class="h-full flex flex-col">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-medium">${modelName} 配置</h3>
<button class="text-gray-400 hover:text-gray-600" onclick="closeConfig()">
<i class="ri-close-line"></i>
</button>
</div>
<div class="flex-1 overflow-y-auto space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">输入参数</label>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm">输入格式</span>
<select class="text-sm border border-gray-300 rounded px-2 py-1">
<option>JSON</option>
<option>Image</option>
<option>Text</option>
</select>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">批处理大小</span>
<input type="number" value="1" class="text-sm border border-gray-300 rounded px-2 py-1 w-16">
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">模型参数</label>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm">置信度阈值</span>
<input type="range" min="0" max="1" step="0.01" value="0.5" class="flex-1 mx-2">
<span class="text-sm w-12">0.5</span>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">最大输出长度</span>
<input type="number" value="512" class="text-sm border border-gray-300 rounded px-2 py-1 w-20">
</div>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">运行时配置</label>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm">GPU 加速</span>
<div class="relative">
<input type="checkbox" checked class="sr-only">
<div class="w-10 h-6 bg-primary rounded-full shadow-inner"></div>
<div class="absolute w-4 h-4 bg-white rounded-full shadow top-1 right-1 transition-transform"></div>
</div>
</div>
<div class="flex items-center justify-between">
<span class="text-sm">缓存结果</span>
<div class="relative">
<input type="checkbox" class="sr-only">
<div class="w-10 h-6 bg-gray-300 rounded-full shadow-inner"></div>
<div class="absolute w-4 h-4 bg-white rounded-full shadow top-1 left-1 transition-transform"></div>
</div>
</div>
</div>
</div>
</div>
<div class="pt-4 border-t border-gray-200">
<button class="w-full bg-primary text-white py-2 rounded-button hover:bg-blue-700 transition-colors !rounded-button">
应用配置
</button>
</div>
</div>
`;
}
window.removeNode = function(button) {
const node = button.closest('.model-node');
const nodeId = node.dataset.nodeId;
connections = connections.filter(conn => 
conn.startNode !== node && conn.endNode !== node
);
node.remove();
updateConnections();
if (selectedNode === node) {
selectedNode = null;
configPanel.innerHTML = `
<div class="text-center text-gray-400 h-full flex flex-col items-center justify-center">
<div class="w-12 h-12 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
<i class="ri-settings-3-line text-xl"></i>
</div>
<p class="text-sm">选择模型节点查看和配置参数</p>
</div>
`;
}
};
window.closeConfig = function() {
if (selectedNode) {
selectedNode.classList.remove('ring-2', 'ring-primary');
selectedNode = null;
}
configPanel.innerHTML = `
<div class="text-center text-gray-400 h-full flex flex-col items-center justify-center">
<div class="w-12 h-12 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
<i class="ri-settings-3-line text-xl"></i>
</div>
<p class="text-sm">选择模型节点查看和配置参数</p>
</div>
`;
};
canvas.addEventListener('click', function(e) {
if (e.target === canvas || e.target === canvas.firstElementChild) {
if (selectedNode) {
selectedNode.classList.remove('ring-2', 'ring-primary');
selectedNode = null;
closeConfig();
}
isConnecting = false;
connectionStart = null;
}
});
});
</script>
<script id="categoryToggleScript">
function toggleCategory(categoryId) {
const models = document.getElementById(categoryId + '-models');
const arrow = document.getElementById(categoryId + '-arrow');
if (models.classList.contains('hidden')) {
models.classList.remove('hidden');
arrow.style.transform = 'rotate(180deg)';
} else {
models.classList.add('hidden');
arrow.style.transform = 'rotate(0deg)';
}
}
</script>
<script id="pipelineActionsScript">
document.addEventListener('DOMContentLoaded', function() {
const testBtn = document.getElementById('testPipeline');
const saveBtn = document.getElementById('savePipeline');
const deployBtn = document.getElementById('deployPipeline');
const clearBtn = document.getElementById('clearCanvas');
testBtn.addEventListener('click', function() {
const nodes = document.querySelectorAll('.model-node');
if (nodes.length === 0) {
alert('请先添加模型节点');
return;
}
this.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>测试中...';
this.disabled = true;
setTimeout(() => {
this.innerHTML = '<i class="ri-play-line mr-1"></i>测试运行';
this.disabled = false;
alert('流水线测试完成！所有模型运行正常。');
}, 3000);
});
saveBtn.addEventListener('click', function() {
const nodes = document.querySelectorAll('.model-node');
if (nodes.length === 0) {
alert('请先添加模型节点');
return;
}
this.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>保存中...';
this.disabled = true;
setTimeout(() => {
this.innerHTML = '<i class="ri-save-line mr-1"></i>保存';
this.disabled = false;
alert('流水线已保存成功！');
}, 2000);
});
deployBtn.addEventListener('click', function() {
const nodes = document.querySelectorAll('.model-node');
if (nodes.length === 0) {
alert('请先添加模型节点');
return;
}
this.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>部署中...';
this.disabled = true;
setTimeout(() => {
this.innerHTML = '<i class="ri-rocket-line mr-1"></i>部署';
this.disabled = false;
alert('流水线部署成功！服务已启动。');
}, 4000);
});
clearBtn.addEventListener('click', function() {
if (confirm('确定要清空画布吗？此操作不可撤销。')) {
document.querySelectorAll('.model-node').forEach(node => node.remove());
document.getElementById('connectionSvg').innerHTML = document.getElementById('connectionSvg').querySelector('defs').outerHTML;
document.getElementById('configPanel').innerHTML = `
<div class="text-center text-gray-400 h-full flex flex-col items-center justify-center">
<div class="w-12 h-12 mx-auto mb-3 rounded-full bg-gray-100 flex items-center justify-center">
<i class="ri-settings-3-line text-xl"></i>
</div>
<p class="text-sm">选择模型节点查看和配置参数</p>
</div>
`;
document.getElementById('canvasContainer').innerHTML = `
<div class="text-center text-gray-400 mt-20">
<div class="w-16 h-16 mx-auto mb-4 rounded-full bg-gray-100 flex items-center justify-center">
<i class="ri-drag-drop-line text-2xl"></i>
</div>
<p class="text-lg">拖拽模型组件到此处开始构建流水线</p>
<p class="text-sm mt-2">从左侧选择合适的模型组件，拖拽到画布中进行组合</p>
</div>
`;
}
});
});
</script>
<script id="searchScript">
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('modelSearch');
searchInput.addEventListener('input', function() {
const searchTerm = this.value.toLowerCase();
const modelItems = document.querySelectorAll('.model-item');
modelItems.forEach(item => {
const modelName = item.querySelector('.text-sm.font-medium').textContent.toLowerCase();
const modelDesc = item.querySelector('.text-xs.text-gray-600').textContent.toLowerCase();
if (modelName.includes(searchTerm) || modelDesc.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
});
});
</script>
</body>
</html>